//go:build ignore

package main

import (
	"bufio"
	"bytes"
	"errors"
	"flag"
	"fmt"
	"go/format"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
)

var (
	TextFlagPath = "src/runtime/textflag.h"
	TextFlagName = filepath.Base(TextFlagPath)
)

var (
	download = flag.Bool("download", false, "download new version of "+TextFlagName)
	version  = flag.String("version", runtime.Version(), "go version to download file from")
	textflag = flag.String("textflag", TextFlagName, "path to "+TextFlagName)
	output   = flag.String("output", "", "path to output file")
)

func main() {
	if err := mainerr(); err != nil {
		log.Fatal(err)
	}
}

func mainerr() error {
	flag.Parse()

	// Download new version, if requested.
	if *download {
		if err := DownloadGoSourceFile(*textflag, *version, TextFlagPath); err != nil {
			return err
		}
		log.Printf("downloaded %q from version %s to %q", TextFlagPath, *version, *textflag)
	}

	// Parse text flags header.
	fs, err := ParseFile(*textflag)
	if err != nil {
		return err
	}

	// Determine output.
	w := os.Stdout
	if *output != "" {
		f, err := os.Create(*output)
		if err != nil {
			return err
		}
		defer f.Close()
		w = f
	}

	// Generate code and format it.
	buf := bytes.NewBuffer(nil)
	PrintFlagAttributes(buf, fs)

	src, err := format.Source(buf.Bytes())
	if err != nil {
		return err
	}

	// Write output.
	_, err = w.Write(src)
	if err != nil {
		return err
	}

	return nil
}

// DownloadGoSourceFile downloads a Go source file from a specific version.
func DownloadGoSourceFile(outpath, v, srcpath string) (err error) {
	// Download from github.
	url := "https://github.com/golang/go/raw/" + v + "/" + srcpath
	res, err := http.Get(url)
	if err != nil {
		return err
	}
	defer func() {
		if errc := res.Body.Close(); errc != nil && err == nil {
			err = errc
		}
	}()

	// Write to file.
	buf := bytes.NewBuffer(nil)
	fmt.Fprintf(buf, "// Code generated by downloading from %s. DO NOT EDIT.\n\n", url)

	if _, err := io.Copy(buf, res.Body); err != nil {
		return err
	}

	if err := os.WriteFile(outpath, buf.Bytes(), 0o644); err != nil {
		return err
	}

	return nil
}

type Flag struct {
	Doc   []string
	Name  string
	Value int
}

// Parse text flags header format.
func Parse(r io.Reader) ([]Flag, error) {
	var fs []Flag
	var doc []string

	s := bufio.NewScanner(r)
	for s.Scan() {
		line := s.Text()
		switch {
		case strings.Contains(line, "TODO"):
			// skip

		case strings.HasPrefix(line, "// "):
			doc = append(doc, strings.TrimPrefix(line, "// "))

		case strings.HasPrefix(line, "#define"):
			fields := strings.Fields(line)
			if len(fields) != 3 || fields[0] != "#define" {
				return nil, fmt.Errorf("unexpected line format %q", line)
			}
			v, err := strconv.Atoi(fields[2])
			if err != nil {
				return nil, err
			}

			fs = append(fs, Flag{
				Doc:   doc,
				Name:  fields[1],
				Value: v,
			})
			doc = nil

		case line == "" || line == "//":
			doc = nil

		default:
			return nil, errors.New("unexpected format")
		}
	}

	if err := s.Err(); err != nil {
		return nil, err
	}

	return fs, nil
}

// ParseFile parses text flags header file.
func ParseFile(filename string) ([]Flag, error) {
	b, err := os.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	return Parse(bytes.NewReader(b))
}

func PrintFlagAttributes(w io.Writer, fs []Flag) {
	_, self, _, _ := runtime.Caller(0)
	fmt.Fprintf(w, "// Code generated by %s. DO NOT EDIT.\n\n", filepath.Base(self))
	fmt.Fprintf(w, "package attr\n")

	// Attribute constants.
	fmt.Fprintf(w, "\n// Attribute values defined in %s.\n", TextFlagName)
	fmt.Fprintf(w, "const (\n")
	for _, f := range fs {
		for _, line := range f.Doc {
			fmt.Fprintf(w, "\t// %s\n", line)
		}
		fmt.Fprintf(w, "\t%s Attribute = %d\n\n", f.Name, f.Value)
	}
	fmt.Fprintf(w, ")\n")

	// String map.
	fmt.Fprintf(w, "\nvar attrname = map[Attribute]string{\n")
	for _, f := range fs {
		fmt.Fprintf(w, "\t%s: %q,\n", f.Name, f.Name)
	}
	fmt.Fprintf(w, "}\n")

	// Flag test methods.
	for _, f := range fs {
		fmt.Fprintf(w, "\n// %s reports whether the %s flag is set.\n", f.Name, f.Name)
		fmt.Fprintf(w, "func (a Attribute) %s() bool { return (a & %s) != 0 }\n", f.Name, f.Name)
	}
}
